#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <stdio.h>
#include <time.h>
#include <errno.h>
#include <unistd.h>
#include <netdb.h>
#include <sys/time.h>
#include <sys/socket.h>
#include "network.h"
#include "world/action.h"
#include "util/log.h"
#include "main.h"
#include "common/common_network.h"

#define PULSE_INTERVAL (FPS/2)
#if TOWERHACK_DEBUG
#define DISC_TIMELIMIT (FPS*60)
#else
#define DISC_TIMELIMIT (FPS*10)
#endif

struct addrinfo* serveraddr;
int sockhdl; /* socket handle */
int seq = 1; /* sequence number for outgoing packets */
int seq_in = 0; /* sequence number for incoming packets */
int connected = 0;
int my_uid; /* this clients unique ID which identifies it to the server */
int disc_countdown; // Ticks until we consider ourselves disconnected from the server
int pulse_countdown; // Ticks until we send a new pulse signal

/* initializes the socket and server address info after the user has entered a server host name */
void network_init (const char* servername)
{
	srand (time(0) ^ *( (unsigned*) servername));

	log_debug ("network_init (%s)", servername);

	if (!connected) {		
		struct addrinfo hints;
		int error;

		memset(&hints, 0, sizeof hints);
		hints.ai_family = AF_INET;
		hints.ai_socktype = SOCK_DGRAM;

		error = getaddrinfo(servername, CTXSTR(SERVER_PORT), &hints, &serveraddr);
		if (error != 0) {
			log_fatal ("Could not getaddrinfo for \"%s\".", servername);
		}
		
		sockhdl = socket(serveraddr->ai_family, serveraddr->ai_socktype,
		                 serveraddr->ai_protocol);
		if (sockhdl < 0) {
			log_fatal ("Could not create client dgram socket, errno=%d.", errno);
		}
		
		error = connect(sockhdl, serveraddr->ai_addr, serveraddr->ai_addrlen);
		if (error != 0) {
			log_fatal ("Could not connect() to %s.", servername);
		}
		
		freeaddrinfo (serveraddr);

		seq = 1;
		seq_in = 0;
		connected = 1;
		my_uid = rand();
		disc_countdown = DISC_TIMELIMIT;
		pulse_countdown = PULSE_INTERVAL;
	}
}

/* no-op message (I'm still online) */
void send_pulse ()
{
	CtoSmessage pulsemsg;
	memset (&pulsemsg, 0, sizeof(pulsemsg));
	pulsemsg.type = CtoS_PULSE;
	send_CtoSmessage (&pulsemsg);
}

void network_update ()
{
	disc_countdown--;
	pulse_countdown--;
	
	if (disc_countdown <= 0) {
		log_fatal ("Disconnected from server.");
	}
	
	if (pulse_countdown <= 0) {
		send_pulse ();
		pulse_countdown = PULSE_INTERVAL;
	}
}

void network_cleanup ()
{
	int error;
	
	error = shutdown (sockhdl, SHUT_RDWR);
	if (error != 0) {
		log_fatal ("Could not shutdown socket.");
	}
	
	error = close (sockhdl);
	if (error != 0) {
		log_fatal ("Could not close socket.");
	}
	
	freeaddrinfo (serveraddr);
	connected = 0;
}

/*
 * The following 4 functions are called when the corresponding
 * message is received from the server.
 * They go on to call the callback functions defined in network.h.
 * Join callbacks are called after the join, leave callbacks
 * BEFORE the leave. So the callbacks have access to the data
 * (nicknames etc.) either way.
 */
void insert_client (int index, char* nickname)
{
	strcpy (the_network.client[index].nickname, nickname);
	if (notify_clientjoin != 0) notify_clientjoin (index);
}

void insert_player (int owneridx, int index, char* nickname, int character)
{
	the_network.owner[index] = owneridx;
	strcpy (the_network.player[index].nickname, nickname);
	the_network.player[index].character_type = character;
	if (notify_playerjoin != 0) notify_playerjoin (index);
}

void remove_client (int index)
{
	int i;
	
	if (notify_clientleave != 0) notify_clientleave (index);
	
	for (i = 0; i < MAX_CLIENTS; i++) {
		if (the_network.owner[i] == index) {
			memset (&the_network.player[i], 0, sizeof (Character));
		}
	}
	memset (&the_network.client[index], 0, sizeof (Client));
}

void remove_player (int index)
{
	if (notify_playerleave != 0) notify_playerleave (index);
	memset (&the_network.player[index], 0, sizeof (Character));
}

/* ALL SEND AND RECEIVE METHODS transform the structs that are sent back and
  forth into a TEXT BASED human readable protocol and parse it. */

/* Send client to server message. UID in msg is ignored and my_uid is substituted! */
void send_CtoSmessage (CtoSmessage* msg)
{
	int error;
	char buffer [BUFSIZE];
	memset (buffer, 0, sizeof(buffer));
	
	int written;
	
	switch (msg->type) {
	
	case CtoS_PULSE:
		sprintf (buffer, "%d 0 PULSE", my_uid);
		break;

	case CtoS_HELLO:
		sprintf (buffer, "%d 0 HELLO \"%s\"\n", my_uid, msg->nickname);
		break;

	case CtoS_REPEAT:
		sprintf (buffer, "%d 0 REPEAT %d\n", my_uid, msg->seq);
		break;
	
	case CtoS_CHAT:  /* (char* line) contains chat message to be sent to everyone */
		sprintf (buffer, "%d %d CHAT \"%s\"\n", my_uid, seq, msg->line);
		break;
	
	case CtoS_LOBBY_ADDPLAYER:
		sprintf (buffer, "%d %d LOBBY_ADDPLAYER \"%s\" %d\n", my_uid, seq, msg->nickname, msg->data.character_type);
		break;
	
	case CtoS_LOBBY_SETPLAYERDATA:
		sprintf (buffer, "%d %d LOBBY_SETPLAYERDATA %d \"%s\" %d\n", my_uid, seq, msg->slot, msg->nickname, msg->data.character_type);
		break;
	
	case CtoS_LOBBY_REMOVEPLAYER:
		sprintf (buffer, "%d %d LOBBY_REMOVEPLAYER %d\n", my_uid, seq, msg->slot);
		break;
	
	case CtoS_LOBBY_READY:
		sprintf (buffer, "%d %d LOBBY_READY\n", my_uid, seq);
		break;
	
	case CtoS_LOBBY_NOTREADY:
		sprintf (buffer, "%d %d LOBBY_NOTREADY\n", my_uid, seq);
		break;
	
	case CtoS_INGAME_ACTION:
		written = sprintf (buffer, "%d %d INGAME_ACTION %d %d %d", 
			my_uid, seq, msg->action.type, msg->action.actor, msg->action.start_time);
		
		switch (msg->action.type) { // append action type specific data
		
		case ACTION_TURN:
			sprintf (buffer+written, " %d\n", msg->action.turn->direction);
			break;
			
		case ACTION_ATTACK_CLOSE:
			sprintf (buffer+written, " %d\n", msg->action.cattack->weapon);
			break;
		
		case ACTION_ATTACK_RANGE:
			sprintf (buffer+written, " %d\n", msg->action.rattack->weapon);
			break;
		
		case ACTION_TRAVERSE:
			sprintf (buffer+written, " %d %d %d\n",
			         XYZ(msg->action.traverse->target));
			break;
		
		case ACTION_INTERACT:
			sprintf (buffer+written, " %d\n", msg->action.interact->target);
			break;
		
		default:
			sprintf (buffer+written, "\n");
			break;
		
		}
		break;
	
	case CtoS_INGAME_SETINVENTORY:
		sprintf (buffer, "%d %d INGAME_SETINVENTORY %d %d %d %d %d %d\n", my_uid, seq, msg->character, msg->inventory.slot[0], msg->inventory.slot[1], msg->inventory.slot[2], msg->inventory.slot[3], msg->inventory.slot[4]);
		break;
	
	default:
		log_fatal ("Failed to send_CtoSmessage of unknown type %d.", msg->type);
		break;
	
	}
	
	#if TOWERHACK_DEBUG_NETWORK
	if (msg->type != CtoS_PULSE) {
		log_debug ("SEND CtoS: %s", buffer);
	}
	#endif
	
	error = send (sockhdl, buffer, sizeof(buffer), 0);
	if (error != sizeof(buffer)) {
		log_fatal ("Error sending data to server.");
	}
	
	// decide whether to increment seq
	switch (msg->type) {
		case CtoS_PULSE:
		case CtoS_HELLO:
			break;
		default:
			seq++;
	}
}

void send_repeat (int seq)
{
	CtoSmessage repeatmsg;
	memset (&repeatmsg, 0, sizeof(repeatmsg));
	repeatmsg.type = CtoS_REPEAT;
	repeatmsg.seq = seq;
	send_CtoSmessage (&repeatmsg);
}
	
StoCmsgType parsetype (char** token)
{
	char* typestr = *token;
	while (isspace(*typestr)) typestr++;
	
	#define TRYPARSE(STR) if (strncmp(typestr, #STR, sizeof(#STR)-1) == 0) { *token = typestr + sizeof(#STR)-1; return StoC_##STR; }
	
	TRYPARSE(PULSE) else
	TRYPARSE(KICK) else
	TRYPARSE(NOTIFY) else
	TRYPARSE(CHAT) else
	TRYPARSE(ALLINFO) else
	TRYPARSE(LOBBY_NEWCLIENT) else
	TRYPARSE(LOBBY_NEWPLAYER) else
	TRYPARSE(LOBBY_PLAYERLEAVE) else
	TRYPARSE(LOBBY_CLIENTLEAVE) else
	TRYPARSE(LOBBY_SEEDGAME) else
	TRYPARSE(LOBBY_CLIENTREADY) else
	TRYPARSE(LOBBY_CLIENTNOTREADY) else
	TRYPARSE(LOBBY_STARTGAME) else
	TRYPARSE(INGAME_ACTION) else
	TRYPARSE(INGAME_DAMAGE) else
	TRYPARSE(INGAME_DIE) else
	TRYPARSE(INGAME_SETCHARACTER) else
	TRYPARSE(INGAME_OVER) else
		log_debug ("Failed to parsetype \"%s\".\n", typestr);
	
	return StoC_ILLEGALMSG;
}

/* Receive server to client message */
void receive_StoCmessage (StoCmessage* msg)
{
	char buffer [BUFSIZE];
	char* readptr = buffer;
	fd_set set;
	struct timeval timeout;
	int sseq;
	int error;
	int parsed_value = 0;
	
tryagain:	
	readptr = buffer;
	memset (buffer, 0, sizeof(buffer));
	memset (&timeout, 0, sizeof(timeout));
	FD_ZERO (&set);
	FD_SET (sockhdl, &set);
	
	error = select(sockhdl+1, &set, 0, 0, &timeout);
	if (error < 0) {
		log_fatal ("Error on select socket.");
	}
	
	if (!FD_ISSET(sockhdl, &set)) {
		msg->type = StoC_NOMSG;
		return;
	}

	error = recv(sockhdl, buffer, sizeof(buffer), 0);
	if (error != sizeof(buffer)) {
		log_fatal ("Error on receive from socket.");
	}
	
	common_network_parse_int (&sseq, &readptr); // FIXME: error handling
	if (sseq != 0) {
		if (sseq > seq_in+1) {
			send_repeat (seq_in+1);
			msg->type = StoC_NOMSG;
	
			log_debug ("RECEIVE and REPEAT (out of seq): %s", buffer);
			return;
		}
		seq_in++;
	}
	
	disc_countdown = DISC_TIMELIMIT; // do not consider message type for this
	
	msg->type = parsetype (&readptr);
	
	#if TOWERHACK_DEBUG_NETWORK
	if (msg->type != StoC_PULSE) {
		log_debug ("RECEIVE: %s", buffer);
	}
	#endif
	
	int type, actor, start_time, dir, x, y, z, index;
	
	switch (msg->type) {
	case StoC_PULSE: // only resets disc_coundown (see above), 
		goto tryagain; // do not pass through to outer app layers
	
	case StoC_KICK:
		log_fatal ("You got kicked from the server.");
		break;
	
	case StoC_NOTIFY:
		common_network_parse_string (msg->line, &readptr);
		break;
	
	case StoC_CHAT:
		common_network_parse_string (msg->line, &readptr);
		break;
	
	case StoC_ALLINFO:
		common_network_parse_int (&msg->allinfo.state, &readptr); // FIXME: error handling
		for (int i = 0; i < MAX_CLIENTS; i++)  {
			common_network_parse_string (msg->allinfo.client[i].nickname, &readptr);
			common_network_parse_int (&msg->allinfo.client[i].ready, &readptr); // FIXME: error handling
			common_network_parse_int (&msg->allinfo.client[i].uid,   &readptr); // FIXME: error handling
		}
		for (int i = 0; i < MAX_CLIENTS; i++) {
			common_network_parse_string (msg->allinfo.player[i].nickname, &readptr);
			common_network_parse_int (&parsed_value, &readptr); // FIXME: error handling
			msg->allinfo.player[i].character_type = parsed_value;
		}
		for (int i = 0; i < MAX_CLIENTS; i++) {
			common_network_parse_int (&msg->allinfo.owner[i], &readptr); // FIXME: error handling
		}
		if (msg->allinfo.state != STATE_LOBBY) {
			log_fatal ("Server is not in lobby so you cannot connect right now.");
		}
		memcpy (the_network.client, msg->allinfo.client, sizeof(the_network.client));
		memcpy (the_network.player, msg->allinfo.player, sizeof(the_network.player));
		memcpy (the_network.owner, msg->allinfo.owner, sizeof(the_network.owner));
		goto tryagain;
		
	case StoC_LOBBY_NEWCLIENT:
		common_network_parse_int (&msg->clientidx, &readptr); // FIXME: error handling
		common_network_parse_string (msg->nickname, &readptr);
		insert_client (msg->clientidx, msg->nickname);
		goto tryagain;
	
	case StoC_LOBBY_NEWPLAYER:
		common_network_parse_int (&msg->owner,     &readptr); // FIXME: error handling
		common_network_parse_int (&msg->playeridx, &readptr); // FIXME: error handling
		common_network_parse_string (msg->player.nickname, &readptr);
		common_network_parse_int (&parsed_value,   &readptr); // FIXME: error handling
		msg->player.character_type = parsed_value;
		insert_player (msg->owner, msg->playeridx, msg->player.nickname, msg->player.character_type);
		goto tryagain;
	
	case StoC_LOBBY_PLAYERLEAVE:
		common_network_parse_int (&msg->playeridx, &readptr); // FIXME: error handling
		remove_player (msg->playeridx);
		goto tryagain;
	
	case StoC_LOBBY_CLIENTLEAVE:
		common_network_parse_int (&msg->clientidx, &readptr); // FIXME: error handling
		remove_client (msg->clientidx);
		goto tryagain;
	
	case StoC_LOBBY_SEEDGAME:
		common_network_parse_ulong (&msg->seed,       &readptr); // FIXME: error handling
		common_network_parse_int (&msg->n_characters, &readptr); // FIXME: error handling
		break;
	
	case StoC_LOBBY_CLIENTREADY:
		common_network_parse_int (&msg->clientidx, &readptr); // FIXME: error handling
		break;		
	
	case StoC_LOBBY_CLIENTNOTREADY:
		common_network_parse_int (&msg->clientidx, &readptr); // FIXME: error handling
		break;
	
	case StoC_LOBBY_STARTGAME:
		break;
	
	case StoC_INGAME_ACTION:
		common_network_parse_int (&type,       &readptr); // FIXME: error handling
		common_network_parse_int (&actor,      &readptr); // FIXME: error handling
		common_network_parse_int (&start_time, &readptr); // FIXME: error handling
		
		int weapon;
		switch (type) {
		
		case ACTION_TURN:
			common_network_parse_int (&dir, &readptr); // FIXME: error handling
			msg->action = action_alloc_turn (actor, start_time, dir);
			break;
			
		case ACTION_ATTACK_CLOSE:
			common_network_parse_int (&weapon, &readptr); // FIXME: error handling
			msg->action = action_alloc_closeattack (actor, start_time, weapon);
			break;
		
		case ACTION_ATTACK_RANGE:
			common_network_parse_int (&weapon, &readptr); // FIXME: error handling
			msg->action = action_alloc_rangeattack (actor, start_time, weapon);
			break;
			
		case ACTION_TRAVERSE:
			common_network_parse_int (&x, &readptr); // FIXME: error handling
			common_network_parse_int (&y, &readptr); // FIXME: error handling
			common_network_parse_int (&z, &readptr); // FIXME: error handling
			msg->action = action_alloc_traverse (actor, start_time,
			                                     (Coord) {x,y,z});
			break;
			
		case ACTION_INTERACT:
			common_network_parse_int (&index, &readptr); // FIXME: error handling
			msg->action = action_alloc_interact (actor, start_time, index);
			break;
			
		default:
			msg->action = NO_ACTION(actor);
			break;
		
		}
		
		msg->action.type = type;
		msg->action.actor = actor;
		msg->action.start_time = start_time;
		break;
	
	case StoC_INGAME_DAMAGE:
		common_network_parse_int (&msg->damage.time,     &readptr); // FIXME: error handling
		common_network_parse_int (&msg->damage.where.x,  &readptr); // FIXME: error handling
		common_network_parse_int (&msg->damage.where.y,  &readptr); // FIXME: error handling
		common_network_parse_int (&msg->damage.where.z,  &readptr); // FIXME: error handling
		common_network_parse_int (&msg->damage.attacker, &readptr); // FIXME: error handling
		common_network_parse_int (&msg->damage.victim,   &readptr); // FIXME: error handling
		common_network_parse_int (&msg->damage.weapon,   &readptr); // FIXME: error handling
		common_network_parse_int (&msg->damage.damage,   &readptr); // FIXME: error handling
		common_network_parse_int (&msg->damage.hp,       &readptr); // FIXME: error handling
		break;
	
	case StoC_INGAME_DIE:
		common_network_parse_int (&msg->damage.time,     &readptr); // FIXME: error handling
		common_network_parse_int (&msg->damage.where.x,  &readptr); // FIXME: error handling
		common_network_parse_int (&msg->damage.where.y,  &readptr); // FIXME: error handling
		common_network_parse_int (&msg->damage.where.z,  &readptr); // FIXME: error handling
		common_network_parse_int (&msg->damage.attacker, &readptr); // FIXME: error handling
		common_network_parse_int (&msg->damage.victim,   &readptr); // FIXME: error handling
		common_network_parse_int (&msg->damage.weapon,   &readptr); // FIXME: error handling
		common_network_parse_int (&msg->damage.damage,   &readptr); // FIXME: error handling
		common_network_parse_int (&msg->damage.hp,       &readptr); // FIXME: error handling
		break;
		
	case StoC_INGAME_SETCHARACTER:
		common_network_parse_int (&msg->who,               &readptr); // FIXME: error handling
		common_network_parse_int (&msg->level,             &readptr); // FIXME: error handling
		common_network_parse_int (&msg->hp,                &readptr); // FIXME: error handling
		common_network_parse_int (&msg->inventory.slot[0], &readptr); // FIXME: error handling
		common_network_parse_int (&msg->inventory.slot[1], &readptr); // FIXME: error handling
		common_network_parse_int (&msg->inventory.slot[2], &readptr); // FIXME: error handling
		common_network_parse_int (&msg->inventory.slot[3], &readptr); // FIXME: error handling
		common_network_parse_int (&msg->inventory.slot[4], &readptr); // FIXME: error handling
		break;
	
	case StoC_INGAME_OVER:
		common_network_parse_int (&msg->winner, &readptr); // FIXME: error handling
		break;
	
	default:
		break;
	
	}
}
